﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Rosetta.Default
{
    [DebuggerDisplay("AddIn {Name}")]
    public abstract class AddIn : IAddIn
    {
        #region Fields

        private readonly Dictionary<string, MethodEntry> methodEntries = new Dictionary<string, MethodEntry>();
        private readonly Func<Delegate, bool> methodValidator;

        private Type[] _ArgumentTypesInternal;
        private Type[] _ReturnTypesInternal;

        #endregion

        #region Constructors

        internal AddIn([NotNull] string name, [NotNull] Func<Delegate, bool> methodValidator)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }
            if (methodValidator == null)
            {
                throw new ArgumentNullException("methodValidator");
            }

            Name = name;
            this.methodValidator = methodValidator;
            Category = "Other";
        }

        #endregion

        #region Public Properties

        [NotNull]
        public string Category { get; protected set; }

        [CanBeNull]
        public string Description { get; protected set; }

        [NotNull]
        public string Name { get; private set; }

        #endregion

        #region Internal Properties

        [NotNull]
        internal Delegate this[[NotNull] string methodName]
        {
            get
            {
                if (string.IsNullOrEmpty(methodName))
                {
                    throw new ArgumentNullException("methodName");
                }

                return methodEntries[methodName].Method;
            }
        }

        internal Type[] ArgumentTypesInternal
        {
            get
            {
                if (_ArgumentTypesInternal == null)
                {
                    Initialize();
                }
                return _ArgumentTypesInternal;
            }
        }

        internal Type[] ReturnTypesInternal
        {
            get
            {
                if (_ReturnTypesInternal == null)
                {
                    Initialize();
                }
                return _ReturnTypesInternal;
            }
        }

        #endregion

        #region Public Methods

        [NotNull]
        public IMethodEntry GetMethod([NotNull] string methodName)
        {
            if (string.IsNullOrEmpty(methodName))
            {
                throw new ArgumentNullException("methodName");
            }
            return methodEntries[methodName];
        }

        [NotNull]
        public IMethodEntry[] GetMethodsByArgumentType([NotNull] Type argumentType)
        {
            if (argumentType == null)
            {
                throw new ArgumentNullException("argumentType");
            }
            if (!(this is IAccepts))
            {
                throw new InvalidOperationException("AddIn can't accept arguments.");
            }

            // Check each entry's argumentTypes and return the methods
            return methodEntries.Values.Where(m => m.ArgumentTypes.Contains(argumentType)).ToArray();
        }

        [NotNull]
        public IMethodEntry[] GetMethodsByArgumentTypes([NotNull] Type[] argumentTypes)
        {
            if (argumentTypes == null)
            {
                throw new ArgumentNullException("argumentTypes");
            }
            if (!(this is IAccepts))
            {
                throw new InvalidOperationException("AddIn can't accept arguments.");
            }

            // Check each entry's argumentTypes and return the methods
            return methodEntries.Values.Where(m => argumentTypes.Any(argumentType => m.ArgumentTypes.Contains(argumentType))).ToArray();
        }

        [NotNull]
        public IMethodEntry[] GetMethodsByReturnType([NotNull] Type returnType)
        {
            if (returnType == null)
            {
                throw new ArgumentNullException("returnType");
            }
            if (!(this is IReturns))
            {
                throw new InvalidOperationException("AddIn can't have return types.");
            }

            // Check each entry's returnTypes and return the methods
            return methodEntries.Values.Where(m => m.ReturnTypes.Contains(returnType)).ToArray();
        }

        [NotNull]
        public IMethodEntry[] GetMethodsByReturnTypes([NotNull] Type[] returnTypes)
        {
            if (returnTypes == null)
            {
                throw new ArgumentNullException("returnTypes");
            }
            if (!(this is IReturns))
            {
                throw new InvalidOperationException("AddIn can't have return types.");
            }

            // Check each entry's returnTypes and return the methods
            return methodEntries.Values.Where(m => returnTypes.Any(returnType => m.ReturnTypes.Contains(returnType))).ToArray();
        }

        public bool HasMethod([NotNull] string methodName)
        {
            if (string.IsNullOrEmpty(methodName))
            {
                throw new ArgumentNullException("methodName");
            }

            return methodEntries.ContainsKey(methodName);
        }

        #endregion

        #region Internal Methods

        internal void AddMethod([NotNull] string name, [NotNull] Delegate method, string[] argumentNames)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }
            if (!methodValidator(method))
            {
                throw new ArgumentException("Invalid method", "method");
            }

            methodEntries.Add(name, new MethodEntry(name, method, argumentNames));
        }

        internal void Initialize()
        {
            var argumentTypes = new List<Type>();
            var returnTypes = new List<Type>();

            foreach (var entry in methodEntries.Values)
            {
                if (entry.HasArgumentTypes)
                {
                    argumentTypes.AddRange(entry.ArgumentTypes);
                }
                if (entry.HasReturnTypes)
                {
                    returnTypes.AddRange(entry.ReturnTypes);
                }
            }

            // Save arguments and returnTypes
            _ArgumentTypesInternal = argumentTypes.Distinct().ToArray();
            _ReturnTypesInternal = returnTypes.Distinct().ToArray();
        }

        #endregion

        #region Class Methods

        internal static bool IsActionType(Type genericType)
        {
            return genericType == typeof(Action<>) || genericType == typeof(Action<,>) || genericType == typeof(Action<,,>) || genericType == typeof(Action<,,,>);
        }

        internal static bool IsConverterFuncType(Type genericType)
        {
            return genericType == typeof(Func<,>) || genericType == typeof(Func<,,>) || genericType == typeof(Func<,,,>) || genericType == typeof(Func<,,,,>);
        }

        #endregion
    }
}